Party mode enhancements#1544
Conversation
Track items now require a tap to expand and reveal the Add and Boost action buttons, resulting in a cleaner search results interface. Artist items continue to show the View Songs button immediately. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Queue items in the guest view can now be tapped to reveal a Boost button, allowing guests to boost an existing track in the queue to play sooner. Only upcoming (non-played) items are interactive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user navigates to the party mode dashboard but does not have access to the configured player, an error message is displayed: "You do not have access to the configured player. Please contact the admin." Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When scanning a party mode QR code with remote access enabled, the remote_id and join code from the URL were not being used to auto-login. The user would see the "enter remote ID" page instead of connecting automatically. The issue was in the fallthrough logic: when both remote_id and join params were present but the combined handler didn't succeed, neither the individual remote_id handler (required !join) nor the join handler (required !remote_id) would run, leaving both params unprocessed. The fix removes the !urlJoinCode guard from the remote_id handler so it always pre-fills the remote ID and stores any join code in session storage for the remote-only auto-connect path to pick up. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-expand behavior - Call new boost_queue_item endpoint with queue_item_id instead of add_to_queue with URI, which was creating duplicate tracks - Only one track can be expanded at a time in both search results and queue views - Collapse expanded track when boost button is pressed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make QR code clickable to copy the party URL to clipboard with an animated overlay bubble for feedback - Remove checkRemoteAccessStatus() which called the admin-only remote_access/info endpoint, causing "Admin access required" errors for regular users on the jukebox dashboard - Add link_copied translation key Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…timing - Use translation keys for copy-to-clipboard feedback instead of raw URL - Use theme CSS variables for copy bubble colors instead of hardcoded values - Scope cursor:pointer to track items only in search results - Consume boost token only after successful server response Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Party mode QR and config composable now listen to CORE_STATE_UPDATED to detect remote access toggles, keeping PROVIDERS_UPDATED for provider load/unload detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…atches - Align token consumption strategy to always consume after successful server response (addToQueue, boostQueueItem, skipCurrentSong) - Remove leftover debug watch statements in PartyModeGuestView - Clear accessError at top of refreshPartyPlayer to avoid duplication Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MarvinSchenkel
left a comment
There was a problem hiding this comment.
LGTM, thanks @apophisnow
| unsubscribe = () => { | ||
| unsubProviders(); | ||
| unsubCoreState(); | ||
| }; |
There was a problem hiding this comment.
Vue has a built in function to handle unsubscribing before unmount. I believe this should be used in place of this manual unsubscribe assignment.
Then the if statement can be removed from the onUnmounted() event.
Edit:
I've seen Marcel use this method inside the 'onMount' event a few times, but there are more examples of it being used outside on its own. I believe both can be used, so worth giving it a go.
| unsubscribe = () => { | |
| unsubProviders(); | |
| unsubCoreState(); | |
| }; | |
| onBeforeUnmount(() => { | |
| unsubProviders(); | |
| unsubCoreState(); | |
| }); |
| .copy-bubble { | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 6px; | ||
| padding: 8px 16px; | ||
| background: rgba(var(--v-theme-surface), 0.9); | ||
| color: rgb(var(--v-theme-success)); | ||
| font-size: 0.9rem; | ||
| font-weight: 600; | ||
| border-radius: 8px; | ||
| white-space: nowrap; | ||
| pointer-events: none; | ||
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); | ||
| } | ||
|
|
||
| .copy-toast-enter-active { | ||
| transition: all 0.2s ease-out; | ||
| } | ||
|
|
||
| .copy-toast-leave-active { | ||
| transition: all 0.3s ease-in; | ||
| } | ||
|
|
||
| .copy-toast-enter-from { | ||
| opacity: 0; | ||
| transform: translate(-50%, -50%) scale(0.8); | ||
| } | ||
|
|
||
| .copy-toast-leave-to { | ||
| opacity: 0; | ||
| transform: translate(-50%, -50%) scale(0.8); | ||
| } | ||
|
|
||
| .qr-display canvas { |
There was a problem hiding this comment.
I'm not sure if we should be creating whole components from scratch. @MarvinSchenkel thoughts on this?
| font-size: 2rem; | ||
| font-weight: 600; | ||
| margin-bottom: 1rem; | ||
| color: rgba(255, 255, 255, 0.9); |
There was a problem hiding this comment.
I believe there are variables that can be used here, so when switching dark/light mode they will change with that theme change. Check out /src/styles/styles.css.
Same comment for line 326 below.
| <!-- Boost action (shown when expanded on upcoming items) --> | ||
| <div v-if="isExpanded && canBoost" class="queue-item-actions"> | ||
| <v-btn | ||
| variant="elevated" | ||
| size="small" | ||
| :loading="boosting" | ||
| :disabled="boostDisabled" | ||
| class="boost-btn" | ||
| :style="{ backgroundColor: boostBadgeColor }" | ||
| @click.stop="$emit('boost', item)" | ||
| > | ||
| <v-icon start size="small">mdi-rocket-launch</v-icon> | ||
| {{ $t("providers.party_mode.boost") }} | ||
| </v-btn> | ||
| </div> |
There was a problem hiding this comment.
I think the visual bug i posted where the boost button was floating outside the queue item is partially due to this DIV sitting outside the 'queue-item-row' DIV above. Moving it inside that and setting max-width on the button should resolve this.
| fetchQueueItems(true); | ||
| }); | ||
|
|
||
| unsubscribeFunctions.value = [unsub1, unsub2, unsub3]; |
There was a problem hiding this comment.
See earlier comment about 'onBeforeUnmount'.
| .access-error-title { | ||
| font-size: 1.5rem; | ||
| font-weight: 600; | ||
| color: rgba(255, 255, 255, 0.9); |
There was a problem hiding this comment.
See earlier comment about using variables.
|
|
||
| const boostQueueItem = async (item: QueueItem) => { | ||
| if (!boostEnabled.value) { | ||
| toast.warning($t("providers.party_mode.boost_disabled")); |
There was a problem hiding this comment.
| toast.warning($t("providers.party_mode.boost_disabled")); | |
| toast.warning($t("providers.party_mode.guest_page.boost_disabled")); |
| if (rateLimitingEnabled.value && boostTokens.value <= 0) { | ||
| const minutesUntilNext = getTimeUntilNextToken(); | ||
| toast.warning( | ||
| $t("providers.party_mode.boost_limit_reached", [minutesUntilNext]), |
There was a problem hiding this comment.
| $t("providers.party_mode.boost_limit_reached", [minutesUntilNext]), | |
| $t("providers.party_mode.guest_page.boost_limit_reached", [minutesUntilNext]), |
| toast.success($t("providers.party_mode.guest_page.item_boosted", [name])); | ||
| } catch (error) { | ||
| console.error("Failed to boost queue item:", error); | ||
| toast.error($t("providers.party_mode.add_to_queue_failed")); |
There was a problem hiding this comment.
| toast.error($t("providers.party_mode.add_to_queue_failed")); | |
| toast.error($t("providers.party_mode.guest_page.add_to_queue_failed")); |
| try { | ||
| const partyPlayerId = await api.sendCommand<string | null>( | ||
| "party_mode/player", | ||
| ); | ||
| partyModeQueueId.value = partyPlayerId; | ||
| if (partyPlayerId && !store.activePlayerId) { | ||
| store.activePlayerId = partyPlayerId; | ||
| } | ||
| console.debug("[GuestView] partyPlayerId:", partyPlayerId); | ||
| console.debug("[GuestView] store.activePlayerId:", store.activePlayerId); | ||
| console.debug("[GuestView] store.activePlayer:", store.activePlayer); | ||
| console.debug("[GuestView] api.players keys:", Object.keys(api.players)); | ||
| console.debug("[GuestView] api.queues keys:", Object.keys(api.queues)); | ||
| console.debug("[GuestView] queue state:", queue.currentQueue.value?.state); | ||
| await refreshPartyPlayer(); | ||
| } catch (error) { | ||
| console.error("Failed to fetch party mode player:", error); | ||
| } |
There was a problem hiding this comment.
I would move the try catch into the 'refreshPartyPlayer' method, that way its all together as one and always handled when called.
This branch is to collect and fix any bugs or enhancements found during beta testing for Party Mode prior to the GA release.